package arkham.data;

import arkham.data.Globals.DimensionalSymbol;
import arkham.data.Globals.MonsterAttribute;
import arkham.data.Globals.MonsterMovement;
import arkham.data.Globals.MonsterPowerActivation;
import arkham.jaxb.monster.*;
import arkham.util.AHLogger;
import arkham.util.ImageUtils;
import arkham.util.XmlTools;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.util.*;

/**
 * This class represents a monster. It encapsulates all the information on each
 * monster tile.
 *
 * @author ctheng
 *
 */
public class Monster implements NamedEntity, IdNumbered
{

	public final static int NO_HORROR_CHECK_RATING = 999;
	public final static int NO_COMBAT_CHECK_RATING = 999;
	public final static int STAT_COMBAT_RATING = 1;
	public final static int STAT_HORROR_RATING = 2;
	public final static int STAT_COMBAT_DAMAGE = 3;
	public final static int STAT_HORROR_DAMAGE = 4;
	public final static int STAT_AWARENESS = 5;
	public final static int STAT_TOUGHNESS = 6;
	private String name;
	private String description;
	private String flavorText;
	private int awareness;
	private int horrorRating;
	private int horrorDamage;
	private int combatRating;
	private int combatDamage;
	private int toughness;
	private BufferedImage image;
	private Color textColor;
	private DimensionalSymbol symbol;
	private MonsterMovement movement;
	private List<MonsterAttribute> attributes;
	private int specialMovementDie;
	private String specialMovementEffect;
	private List<MonsterPower> powers;
	/* Meta-attributes for ease of access */
	private int physDefences, magDefences;
	private int nightmarish, overwhelming;

	private String location;
	private LocationContainer locContainer;
	private int id;

	public Monster(String name) {
		this.name = name;
		description = "";
		flavorText = "";
		attributes = new ArrayList<MonsterAttribute>();
		powers = new ArrayList<MonsterPower>();
		specialMovementDie = 0;
		physDefences = Globals.NONE;
		magDefences = Globals.NONE;
		nightmarish = 0;
		overwhelming = 0;
		location = null;
	}

	/**
	 * Gets the name of the monster
	 *
	 * @return the name of the monster
	 */
	public String getName() {
		return name;
	}

	/**
	 * Gets the description text of the monster's powers
	 *
	 * @return the description of the powers
	 */
	public String getDescription() {
		return description;
	}

	/**
	 * Gets the flavor text on the monster
	 *
	 * @return the flavor text
	 */
	public String getFlavorText() {
		return flavorText;
	}

	/**
	 * Gets the monster's awareness
	 *
	 * @return the awareness
	 */
	public int getAwareness() {
		return awareness;
	}

	/**
	 * Gets the monster's horror rating (NOTE: 999 if no horror check)
	 *
	 * @return the horror rating
	 */
	public int getHorrorRating() {
		return horrorRating;
	}

	/**
	 * Gets the monster's horror damage (NOTE: 0 if special horror damage)
	 *
	 * @return the horror damage
	 */
	public int getHorrorDamage() {
		return horrorDamage;
	}

	/**
	 * Gets the monster's combat rating
	 *
	 * @return the combat rating
	 */
	public int getCombatRating() {
		return combatRating;
	}

	/**
	 * Gets the monster's combat damage (NOTE: 0 if special combat damage)
	 *
	 * @return the combatDamage
	 */
	public int getCombatDamage() {
		return combatDamage;
	}

	/**
	 * Gets the monster's toughness
	 *
	 * @return the toughness
	 */
	public int getToughness() {
		return toughness;
	}

	/**
	 * Gets the image of the monster
	 * @return the image
	 */
	public BufferedImage getImage()
	{
		return image;
	}

	/**
	 * Sets the image of the monster
	 * @param filename the filename of the image
	 */
	public void setImage(String filename)
	{
		if (filename != null && !filename.equals(""))
		{
			BufferedImage img = ImageUtils.loadImage(filename);
			image = ImageUtils.resizeImage(img, 35, 35);

			// sample middle of image
			int sr = 0, sg = 0, sb = 0;
			for (int i = 10; i < 25; i++)
			{
				for (int j = 10; j < 25; j++)
				{
					int rgb = image.getRGB(i, j);
					int red = (rgb >> 16) & 0xFF;
					int green = (rgb >> 8) & 0xFF;
					int blue = rgb & 0xFF;
					sr += red;
					sg += green;
					sb += blue;
				}
			}
			sr /= 225;
			sg /= 225;
			sb /= 225;
			textColor = ImageUtils.getContrastingColor(sr, sg, sb);

			Graphics2D g = image.createGraphics();
			g.setColor(Color.white);
			g.fillRect(23, 0, 12, 12);
			g.fillRect(21, 21, 14, 14);
			g.setColor(Color.black);
			g.drawRect(23, 0, 12, 12);
			g.drawRect(21, 21, 14, 14);
			g.setStroke(new BasicStroke(2, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER));
			g.setColor(movement.getColor());
			g.drawRect(0, 0, 35, 35);
			Font blueCond = ImageUtils.loadFont("fonts/bluecond.ttf");
			g.setFont(blueCond.deriveFont(Font.BOLD, 14));
			g.setColor(Color.red);
			g.drawString(XmlTools.getModifier(awareness), 25, 10);
			g.drawImage(symbol.getImage(), 22, 22, null);
			g.dispose();

		}
	}

	/**
	 * Gets the preferred text color
	 * @return the preferred text color
	 */
	public Color getTextColor()
	{
		return textColor;
	}

	/**
	 * Gets the monster's dimensional symbol
	 *
	 * @return the symbol
	 */
	public DimensionalSymbol getSymbol() {
		return symbol;
	}

	/**
	 * Gets the monster's movement
	 * @return the movement
	 */
	public MonsterMovement getMovement() {
		return movement;
	}

	/**
	 * Gets a list of the monster's attributes
	 * @return the attributes
	 */
	public List<MonsterAttribute> getAttributes() {
		return attributes;
	}

	/**
	 * Adds an attribute to the list of monster attributes
	 * @param attrib the attribute to add
	 */
	public void addAttribute(MonsterAttribute attrib)
	{
	    attributes.add(attrib);
	    switch (attrib)
	    {
	    case PHYS_RES:
	        physDefences = Globals.RESISTANCE;
	        break;
	    case PHYS_IMM:
	        physDefences = Globals.IMMUNITY;
	        break;
	    case MAG_RES:
	        magDefences = Globals.RESISTANCE;
	        break;
	    case MAG_IMM:
	        magDefences = Globals.IMMUNITY;
	        break;
	    case NIGHTMARISH:
	        nightmarish = 1;
	        break;
	    case NIGHTMARISH2:
	        nightmarish = 2;
	        break;
	    case OVERWHELMING:
	        overwhelming = 1;
	        break;
	    case OVERWHELMING2:
	        overwhelming = 2;
	        break;
	    }
	}

	/**
	 * Checks if the monster has an attribute
	 * @param attrib the attribute to check
	 * @return true if the monster has that attribute
	 */
	public boolean hasAttribute(MonsterAttribute attrib)
	{
	    return attributes.contains(attrib);
	}

	/**
	 * Gets this monster's physical defences
	 * @return Globals.NONE, RESISTANCE or IMMUNITY
	 */
	public int getPhysicalDefences()
	{
	    return physDefences;
	}

	/**
     * Gets this monster's magical defences
     * @return Globals.NONE, RESISTANCE or IMMUNITY
     */
	public int getMagicalDefences()
	{
	    return magDefences;
	}

	/**
	 * Gets the amount of nightmarish effect of this monster
	 * @return the nightmarish value
	 */
	public int getNightmarishValue()
	{
	    return nightmarish;
	}

	/**
	 * Gets the amount of overwhelming effect of this monster
	 * @return the overwhelming value
	 */
	public int getOverwhelmingValue()
	{
	    return overwhelming;
	}

	/**
	 * Gets the die roll to trigger special movement. Special movement is
	 * triggered if a 1d6 die roll is equal or higher than the specified
	 * amount.
	 * Set to 0 if the special movement always happens
	 * @return the special movement die
	 */
	public int getSpecialMovementDie() {
		return specialMovementDie;
	}

	/**
	 * Gets the effect of special movement
	 * @return the special movement effect
	 */
	public String getSpecialMovementEffect() {
		return specialMovementEffect;
	}

	/**
	 * Gets the monster's location
	 * @param location the location to set
	 */
	public void setLocation(LocationContainer cont) {
		if (cont != null)
		{
			this.location = cont.getName();
			this.locContainer = cont;
		}
	}

	/**
	 * Gets the monster's location name
	 * @return the location
	 */
	public String getLocationName() {
		return location;
	}

	/**
	 * Gets the monster's location container
	 * @return the location container
	 */
	public LocationContainer getLocation()
	{
		return locContainer;
	}

	/**
	 * Gets the unique id of this monster
	 * @return the id of this monster
	 */
	public int getId()
	{
		return id;
	}

	/**
	 * Sets the unique id of this monster
	 * @param id the id to set
	 */
	public void setId(int id)
	{
		this.id = id;
	}

	@Override
	public String toString()
	{
		return name;
	}

	/**
	 * Gets the list of special combat powers of the monster
	 * @return the special combat powers
	 */
	public List<MonsterPower> getPowers() {
		return powers;
	}

	/**
	 * Gets the specific power that activates as a result of
	 * a certain activation condition
	 * @param act the activation condition
	 * @return the specific monster power, if any, or null if not found
	 */
	public MonsterPower getSpecificPower(MonsterPowerActivation act)
	{
	    for (MonsterPower pow : powers)
	    {
	        if (pow.getActivationCondition().equals(act))
	        {
	            return pow;
	        }
	    }
	    return null;
	}

	public static class MonsterPower {

		private MonsterPowerActivation activation;
		private String type;
		private String details;
		private int amount;

		public MonsterPower() {

		}

		/**
		 * Gets the activation condition of this power
		 *
		 * @return the activation condition
		 */
		public MonsterPowerActivation getActivationCondition() {
			return activation;
		}

		/**
		 *
		 * @param activation
		 *            the activation to set
		 */
		public void setActivation(MonsterPowerActivation activation) {
			this.activation = activation;
		}

		/**
		 * @return the type
		 */
		public String getType() {
			return type;
		}

		/**
		 * @param type the effect to set
		 */
		public void setType(String type) {
			this.type = type;
		}

		/**
		 * Gets the details of this monster power
		 * @return the details
		 */
		public String getDetails()
		{
			return details;
		}

		/**
		 * Set the details of the monster power
		 * @param details the details to set
		 */
		public void setDetails(String details)
		{
			this.details = details;
		}

		/**
		 * Gets the amount of the effect, if applicable
		 * @return the amount
		 */
		public int getAmount()
		{
			return amount;
		}

		/**
		 * Sets the amount of the effect, if applicable
		 * @param amount the amount to set
		 */
		public void setAmount(int amount)
		{
			this.amount = amount;
		}
	}

	/**
	 * Creates the data representation of the monster from the XML info and
	 * performs a stringent validation check to make sure the XML data is
	 * valid.
	 * <br>Will return <b>null</b> on a failure.
	 *
	 * @param xmlboard The XML monster info
	 * @return the data representation of the monster
	 */
	public static Monster createMonsterFromXml(MonsterType xmlmonster) {
		Monster mon = new Monster(xmlmonster.getName());
		boolean requireSpecialHorror = false;
		boolean requireSpecialCombat = false;
		boolean specialHorrorFound = false;
		boolean specialCombatFound = false;

		if (xmlmonster.getDescription() != null)
		{
			mon.description = xmlmonster.getDescription();
		}
		if (xmlmonster.getFlavor() != null)
		{
			mon.flavorText = xmlmonster.getFlavor();
		}
		mon.awareness = xmlmonster.getAlertness();
		mon.horrorRating = xmlmonster.getHorrorRating();
		// load horror check damage if there is a horror check
		if (mon.horrorRating != NO_HORROR_CHECK_RATING)
		{
			mon.horrorDamage = xmlmonster.getHorrorDamage();
			if (mon.horrorDamage < 0)
			{
				AHLogger.error(mon.name + " has negative horror damage.");
				return null;
			}
			else if (mon.horrorDamage == 0)
			{
				requireSpecialHorror = true;
			}
		}
		else
		{
			mon.horrorDamage = -1;
		}
		mon.combatRating = xmlmonster.getCombatRating();
		mon.combatDamage = xmlmonster.getCombatDamage();
		if (mon.combatDamage == 0)
		{
			requireSpecialCombat = true;
		}
		mon.toughness = xmlmonster.getToughness();
		if (!DimensionalSymbol.isValidSymbol(xmlmonster.getSymbol()))
		{
			AHLogger.error(mon.name + " has invalid dimensional symbol: " +
					xmlmonster.getSymbol());
			return null;
		}
		mon.symbol = DimensionalSymbol.getDimensionalSymbol(xmlmonster.getSymbol());
		if (!MonsterMovement.isValidMovement(xmlmonster.getMovement()))
		{
			AHLogger.error(mon.name + " has invalid monster movement: " +
					xmlmonster.getMovement());
			return null;
		}
		mon.movement = MonsterMovement.getMonsterMovement(xmlmonster.getMovement());
		if (mon.movement.equals(MonsterMovement.SPECIAL))
		{
			SpecialMovementType smtype = xmlmonster.getSpecialMovement();
			if (smtype == null)
			{
				AHLogger.error(mon.name + " did not define special movement.");
				return null;
			}
			mon.specialMovementDie = smtype.getDieRoll();
			mon.specialMovementEffect = smtype.getEffect();
		}
		// initialize monster attributes
		for (String attrib : xmlmonster.getAttribute())
		{
			if (MonsterAttribute.isValidAttribute(attrib))
			{
				mon.addAttribute(MonsterAttribute.getAttribute(attrib));
			}
			else
			{
				AHLogger.error(mon.name + " has the invalid attribute : " + attrib);
				return null;
			}
		}
		if (MonsterAttribute.anyConflictingAttributes(mon.attributes))
		{
			return null;
		}
		// initialize special combat powers
		for (SpecialCombatType sctype : xmlmonster.getSpecialCombat())
		{
			String when = sctype.getWhen();
			if (!MonsterPowerActivation.isValidActivation(when))
			{
				AHLogger.error(mon.name + " has special power with invalid activation" +
						" condition: " + when);
				return null;
			}
			MonsterPowerActivation mpact = MonsterPowerActivation.getActivationCondition(when);
			switch (mpact)
			{
			case FAIL_HORROR:
				specialHorrorFound = true;
				break;
			case FAIL_COMBAT:
				specialCombatFound = true;
				break;
			}
			MonsterPower mp = new MonsterPower();
			mp.setActivation(mpact);
			mp.setType(sctype.getType());
			mp.setDetails(sctype.getDetails());
			if (sctype.getAmount() != null)
			{
				mp.setAmount(sctype.getAmount());
			}
			mon.getPowers().add(mp);
		}

		if (requireSpecialHorror && !specialHorrorFound)
		{
			AHLogger.error(mon.name + " must define special horror damage.");
			return null;
		}
		if (requireSpecialCombat && !specialCombatFound)
		{
			AHLogger.error(mon.name + " must define special combat damage.");
			return null;
		}

		mon.setImage(xmlmonster.getImg());
		return mon;
	}
}
